概述
本节是策略权限控制的架构设计核心。在 RBAC(基于角色的接口级权限)基础上,扩展 ACL(基于策略的数据级权限),实现字段粒度的访问控制。重点分析权限系统的数据模型设计、RBAC 与 ACL 的关系,以及如何将策略规则存储到数据库实现动态权限。
RBAC 与 ACL 的定位
权限系统分层架构
┌─────────────────────────────────────────┐
│ 前端页面(菜单权限) │
├─────────────────────────────────────────┤
│ Controller / 路由层(RBAC 接口权限) │
├─────────────────────────────────────────┤
│ 数据库查询层(ACL 策略权限) │ ← 本节重点
│ 字段级控制 + 数据归属判断 │
└─────────────────────────────────────────┘
text
RBAC vs ACL 对比
| 维度 | RBAC | ACL(策略权限) |
|---|---|---|
| 控制粒度 | 接口/路由级别 | 字段/数据级别 |
| 判断依据 | 用户角色 | 策略规则 + 数据条件 |
| 典型场景 | 管理员可访问后台 | 作者只能编辑自己的文章 |
| 通用程度 | 绝大多数应用 | 安全级别要求高的场景 |
| 实现复杂度 | 低 | 高 |
为什么需要 ACL
以博客系统为例,RBAC 无法满足以下需求:
场景:用户 A 更新文章 Post
├── RBAC 角色判断:用户有 "post:update" 权限 → 通过 ✅
├── 但这篇文章的作者不是 A → 应该拒绝 ❌
└── RBAC 无法判断数据归属关系
场景:用户只能修改文章的 content 字段
├── RBAC 角色判断:用户有 "post:update" 权限 → 通过 ✅
├── 但用户修改了 title 字段 → 应该拒绝 ❌
└── RBAC 无法控制字段级别
text
如果用 RBAC 解决上述问题,需要:
- 为每种数据归属关系创建不同的接口
- 在每个接口中手写归属判断逻辑
- 违反 DRY 原则,维护成本极高
数据库模型设计
实体关系分析
策略权限系统涉及的核心实体及其关系:
User ←→ UserRole ←→ Role ←→ RolePolicy ←→ Policy
↓
Role ←→ RolePermission ←→ Permission ←→ Policy
text
关系矩阵
| 关系 | 类型 | 说明 |
|---|---|---|
| User ↔ Role | 多对多 | 一个用户有多个角色,一个角色对应多个用户 |
| Role ↔ Permission | 多对多 | 一个角色有多个权限,一个权限可属于多个角色 |
| Role ↔ Policy | 多对多 | 一个角色有多条策略,一条策略可关联多个角色 |
| Permission ↔ Policy | 一对多 | 一个权限可关联多条策略 |
Policy 表结构设计
策略表需要存储以下关键信息:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
subject | String | 操作的实体/表 | Post、User |
action | String | 操作类型 | read、update、delete |
fields | JSON | 允许操作的字段 | ["title", "content"] |
conditions | JSON | 数据条件 | { "authorId": "{userId}" } |
完整数据模型关系图
┌──────────┐ ┌───────────┐ ┌──────────┐
│ User │────←│ UserRole │→────│ Role │
└──────────┘ └───────────┘ └──────────┘
│
┌──────────┼──────────┐
↓ ↓
┌──────────────┐ ┌──────────────┐
│ RolePolicy │ │RolePermission│
└──────┬───────┘ └──────┬───────┘
↓ ↓
┌──────────────┐ ┌──────────────┐
│ Policy │ │ Permission │
│ - subject │ │ - name │
│ - action │ │ - action │
│ - fields │ └──────────────┘
│ - conditions │
└──────────────┘
text
策略权限的判断流程
查询链路
请求到达 Guard
├── 1. 从装饰器获取当前路由的 Permission 标识
├── 2. 通过 Permission 查询对应的 Policy 列表(接口要求的数据权限)
├── 3. 从用户信息获取角色,查询角色的 Policy(用户已有的数据权限)
└── 4. 对比两组 Policy,使用 CASL Ability 判断是否通过
text
接口侧 Policy(需要什么权限)
// 从 Controller 装饰器读取 Permission
// → 查询 Permission 关联的 Policy
// → 得到接口要求的数据权限列表
const requiredPolicies = [
{ subject: 'Post', action: 'update', fields: ['title', 'content'], conditions: { authorId: 1 } },
];
typescript
用户侧 Policy(拥有什么权限)
// 从用户角色获取关联的 Policy
// → 构建用户的 Ability 实例
const userAbilities = [
ability1, // 来自 Role A 的策略
ability2, // 来自 Role B 的策略
];
typescript
匹配算法
requiredPolicies = [P1, P2, P3, P4] // 接口要求的权限
userAbilities = [A1, A2, A3, A4] // 用户拥有的能力
对每个 Pi,遍历所有 Aj:
if Aj.can(Pi.action, Pi.subject, Pi.fields) === true
→ Pi 通过,从 requiredPolicies 中移除
最终:requiredPolicies 为空 → 权限通过
requiredPolicies 不为空 → 权限拒绝
text
为什么存储到数据库
所有策略规则存储在数据库中,而不是硬编码在代码中:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 硬编码 | 简单快速 | 修改需要改代码、重新部署 |
| 数据库存储 | 动态调整、运行时修改 | 实现复杂度高 |
数据库存储的优势:
- 动态性:通过接口即可调整权限规则,无需重新部署
- 可管理性:管理员通过后台界面管理权限
- 可审计:权限变更记录可追溯
- 可扩展:新增资源类型只需添加数据记录
RBAC + ACL 协同工作
RBAC 和 ACL 不冲突,而是分层协作:
请求进入
↓
RBAC Guard:用户角色是否有接口访问权限?
├── 否 → 403 Forbidden
└── 是 → 继续向下
↓
Policy Guard:用户策略是否有数据操作权限?
├── 否 → 403 Forbidden
└── 是 → 执行业务逻辑
text
关键知识点总结
| 知识点 | 说明 |
|---|---|
| RBAC 局限 | 只能控制接口级别,无法判断数据归属和字段权限 |
| ACL 定位 | 在 RBAC 基础上扩展的数据级权限控制 |
| Policy 表 | 存储 subject、action、fields、conditions 四要素 |
| 数据库存储 | 实现动态权限,通过接口管理而非硬编码 |
| 双层 Guard | RBAC 先判断接口权限,ACL 再判断数据权限 |
| DRY 原则 | 策略权限避免在每个接口重复编写归属判断 |
↑